Layout Animations
Video Summary
In this video, we explore how to use layout animations to animate a green box that gets "maximized" by toggling on a maximized
class:
.wrapper { width: 180px; height: 180px; /* Cosmetic styles omitted for brevity */}
.maximized { position: absolute; inset: 0; width: revert; height: revert;}
By default, we switch immediately from one layout to another, producing a pretty jarring experience:
Unfortunately, there's no way to animate this sort of thing with CSS transitions. We're not sliding properties from one value to another, we're swapping out entire properties!
To solve this problem, we'll need to use a layout animation, Framer Motion's killer feature.
First, we'll make the following change:
import { motion } from 'framer-motion';
function App() { const [isMaximized, setIsMaximized] = React.useState(false);
return ( <> <motion.div layout={true} className={`wrapper ${isMaximized ? 'maximized' : ''}`} />
<button onClick={() => setIsMaximized(!isMaximized)}> Toggle </button> </> );}
The layout
prop tells Framer Motion to magically animate any layout changes, and it does this using a modern, sophisticated take on the FLIP technique.
Understanding the FLIP technique is beyond the scope of this course, and you shouldn't need to understand it in order to use Framer Motion, but if you're curious, I've shared some links under this video where you can learn more.
The “cliff notes” version is that we figure out the size/position of the original state, and then the size/position of the end state. We calculate the "delta" between them in terms of size and position, and then use a CSS transform
to tween from the original state to the new state.
Like we saw in the previous lesson, we can customize the transition with the transition
prop. I've applied my own custom spring:
<motion.div layout={true} transition={SPRING} className={`wrapper ${isMaximized ? 'maximized' : ''}`}/>
const SPRING = { type: 'spring', stiffness: 200, damping: 40,};
Now, I should warn you: Framer Motion has a steep learning curve. It's not as simple as adding a layout
prop and having everything work. If you try to use this in your own projects, you'll likely run into some weird quirks / things not working the way you'd expect.
Framer Motion is incredibly complex, and sometimes, that complexity seeps out and becomes a problem we have to deal with.
The most common issue is when the animated element has content. For example, if we add the text "Hello world", we'll notice that it gets distorted:
The reason this happens is that CSS transforms work by sending the raw pixel data to the graphics card. For example, transform: scale(2)
will stretch everything to double its original size. Essentially, you can think of it like a screenshot of the DOM, an image which is being stretched and warped.
Fortunately, there's a solution. We need to use a nested layout animation:
<motion.div layout={true} transition={SPRING} className={`wrapper ${isMaximized ? 'maximized' : ''}`}> <motion.p layout="position"> Hello world </motion.p></motion.div>
const SPRING = { type: 'spring', stiffness: 200, damping: 40,};
We wrap the text in a new motion
component. I've chosen to use motion.p
because it's the most semantically-appropriate, but this technique works with any DOM element.
We set layout="position"
, because we only want to adjust the element's position, not its size. The layout
property takes 3 values:
"position"
— animate the element's position usingtranslate
."size"
— animate the element's size usingscale
.true
— animate both the position and size.
With that done, the text no longer distorts:
Interestingly, if we inspect the <p>
during the transition, we see that the scale
value is being manipulated:
This paragraph is within a parent <motion.div>
, and that parent is being stretched with scale()
. In order to avoid the text being distorted, we have to apply the opposite scale, cancelling out that transform.
Nested layout components are often necessary, to prevent distortion issues like this.
Now, let's suppose we want to center the text within the teal box. We can do this in the CSS with text-align: center
.
This produces a funky effect:
The reason this happens is because Framer Motion doesn't actually know anything about the position of individual characters. It only knows about the position of the <p>
box. And this box changes size depending on the state:
What we need to do is to shrink the <p>
so that it wraps neatly around the characters. There are several ways to do this, but I prefer to use Flexbox on the parent:
.wrapper { display: flex; justify-content: center; align-items: flex-start;}
With this done, the paragraph shrinkwraps around the text:
When we test it out, things are better, but they're still a bit funky:
The problem here is that transition settings aren't inherited. The parent <motion.div>
has our custom SPRING
settings, but the child doesn't. And so they're both being animated according to different spring settings.
We can fix this by copying the transition
prop to the child:
<motion.div layout={true} transition={SPRING} className={`wrapper ${isMaximized ? 'maximized' : ''}`}> <motion.p layout="position" transition={SPRING} > Hello world </motion.p></motion.div>
const SPRING = { type: 'spring', stiffness: 200, damping: 40,};
With this done, our animation is finally complete. And it looks pretty slick:
Framer Motion is incredibly cool, and it opens a lot of doors in terms of what's possible. At the same time, though, it's not an easy tool to use. The journey with layout animations involves a lot of speed bumps, and it can often be frustrating when the animation isn't doing what we want, and we don't know why.
There's a silver lining though: if it was trivial to do layout animations, everyone would do them. They'd become a standard part of the web, table stakes for a well-built product.
Because they're non-trivial, they're also rare. And because they're rare, they capture user attention. A well-implemented layout animation is a delightful surprise.
And so, even though it's not always smooth sailing, I absolutely think it's worth getting comfortable with Framer Motion.
Phew! We covered a lot of ground in that video 😅. Here are some of the key takeaways:
- We use the
layout
prop to enable layout animations formotion
components. layout
can be set to"position"
,"size"
, ortrue
(for both position and size).- Layout animations use CSS transforms, which essentially treat the element as if it was an image. This can cause distortions, if the element contains text or other elements. We fix this by nesting motion components, to “cancel out” those transformations.
- Framer Motion uses the bounding box for all elements. It doesn't know where the individual characters are within a paragraph. To avoid issues, we should “shrinkwrap” elements around their characters.
- Transition settings aren't inherited; be sure to copy the
transition
prop from the parent to the child, so that they both use the same spring parameters.
Here's the playground from the video. I strongly recommend spending a few minutes tinkering with it. See if you can start building an intuition for how it works:
Code Playground